<?php
/**
 * This file is part of workerman.
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the MIT-LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @author    walkor<walkor@workerman.net>
 * @copyright walkor<walkor@workerman.net>
 * @link      http://www.workerman.net/
 * @license   http://www.opensource.org/licenses/mit-license.php MIT License
 */
namespace Workerman\Protocols;

use Workerman\Connection\TcpConnection;
use Workerman\Protocols\Http\Request;
use Workerman\Protocols\Http\Response;

/**
 * Class Http.
 *
 * @package Workerman\Protocols
 */
class Http {

	/**
	 * Request class name.
	 *
	 * @var string
	 */
	protected static $_requestClass = 'Workerman\Protocols\Http\Request';

	/**
	 * Session name.
	 *
	 * @var string
	 */
	protected static $_sessionName = 'PHPSID';

	/**
	 * Upload tmp dir.
	 *
	 * @var string
	 */
	protected static $_uploadTmpDir = '';

	/**
	 * Open cache.
	 *
	 * @var bool.
	 */
	protected static $_enableCache = true;

	/**
	 * Get or set session name.
	 *
	 * @param null $name        	
	 * @return string
	 */
	public static function sessionName($name = null) {
		if ($name !== null && $name !== '') {
			static::$_sessionName = (string) $name;
		}
		return static::$_sessionName;
	}

	/**
	 * Get or set the request class name.
	 *
	 * @param null $class_name        	
	 * @return string
	 */
	public static function requestClass($class_name = null) {
		if ($class_name) {
			static::$_requestClass = $class_name;
		}
		return static::$_requestClass;
	}

	/**
	 * Enable or disable Cache.
	 *
	 * @param
	 *        	$value
	 */
	public static function enableCache($value) {
		static::$_enableCache = (bool) $value;
	}

	/**
	 * Check the integrity of the package.
	 *
	 * @param string $recv_buffer        	
	 * @param TcpConnection $connection        	
	 * @return int
	 */
	public static function input($recv_buffer, TcpConnection $connection) {
		static $input = array();
		if (! isset($recv_buffer[512]) && isset($input[$recv_buffer])) {
			return $input[$recv_buffer];
		}
		$crlf_pos = \strpos($recv_buffer, "\r\n\r\n");
		if (false === $crlf_pos) {
			// Judge whether the package length exceeds the limit.
			if ($recv_len = \strlen($recv_buffer) >= 16384) {
				$connection->close("HTTP/1.1 413 Request Entity Too Large\r\n\r\n");
				return 0;
			}
			return 0;
		}
		
		$head_len = $crlf_pos + 4;
		$method = \strstr($recv_buffer, ' ', true);
		
		if ($method === 'GET' || $method === 'OPTIONS' || $method === 'HEAD' || $method === 'DELETE') {
			if (! isset($recv_buffer[512])) {
				$input[$recv_buffer] = $head_len;
				if (\count($input) > 512) {
					unset($input[key($input)]);
				}
			}
			return $head_len;
		} else if ($method !== 'POST' && $method !== 'PUT') {
			$connection->close("HTTP/1.1 400 Bad Request\r\n\r\n", true);
			return 0;
		}
		
		$header = \substr($recv_buffer, 0, $crlf_pos);
		$length = false;
		if ($pos = \strpos($header, "\r\nContent-Length: ")) {
			$length = $head_len + (int) \substr($header, $pos + 18, 10);
		} else if (\preg_match("/\r\ncontent-length: ?(\d+)/i", $header, $match)) {
			$length = $head_len + $match[1];
		}
		
		if ($length !== false) {
			if (! isset($recv_buffer[512])) {
				$input[$recv_buffer] = $length;
				if (\count($input) > 512) {
					unset($input[key($input)]);
				}
			}
			return $length;
		}
		
		$connection->close("HTTP/1.1 400 Bad Request\r\n\r\n", true);
		return 0;
	}

	/**
	 * Http decode.
	 *
	 * @param string $recv_buffer        	
	 * @param TcpConnection $connection        	
	 * @return \Workerman\Protocols\Http\Request
	 */
	public static function decode($recv_buffer, TcpConnection $connection) {
		static $requests = array();
		$cacheable = static::$_enableCache && ! isset($recv_buffer[512]);
		if (true === $cacheable && isset($requests[$recv_buffer])) {
			$request = $requests[$recv_buffer];
			$request->connection = $connection;
			$connection->__request = $request;
			$request->properties = array();
			return $request;
		}
		$request = new static::$_requestClass($recv_buffer);
		$request->connection = $connection;
		$connection->__request = $request;
		if (true === $cacheable) {
			$requests[$recv_buffer] = $request;
			if (\count($requests) > 512) {
				unset($requests[key($requests)]);
			}
		}
		return $request;
	}

	/**
	 * Http encode.
	 *
	 * @param string|Response $response        	
	 * @param TcpConnection $connection        	
	 * @return string
	 */
	public static function encode($response, TcpConnection $connection) {
		if (isset($connection->__request)) {
			$connection->__request->session = null;
			$connection->__request->connection = null;
			$connection->__request = null;
		}
		if (\is_scalar($response) || null === $response) {
			$ext_header = '';
			if (isset($connection->__header)) {
				foreach ($connection->__header as $name => $value) {
					if (\is_array($value)) {
						foreach ($value as $item) {
							$ext_header = "$name: $item\r\n";
						}
					} else {
						$ext_header = "$name: $value\r\n";
					}
				}
				unset($connection->__header);
			}
			$body_len = \strlen($response);
			return "HTTP/1.1 200 OK\r\nServer: workerman\r\n{$ext_header}Connection: keep-alive\r\nContent-Type: text/html;charset=utf-8\r\nContent-Length: $body_len\r\n\r\n$response";
		}
		
		if (isset($connection->__header)) {
			$response->withHeaders($connection->__header);
			unset($connection->__header);
		}
		
		if (isset($response->file)) {
			$file = $response->file['file'];
			$offset = $response->file['offset'];
			$length = $response->file['length'];
			$file_size = (int) \filesize($file);
			$body_len = $length > 0 ? $length : $file_size - $offset;
			$response->withHeaders(array(
				'Content-Length' => $body_len,
				'Accept-Ranges' => 'bytes'
			));
			if ($offset || $length) {
				$offset_end = $offset + $body_len - 1;
				$response->header('Content-Range', "bytes $offset-$offset_end/$file_size");
			}
			if ($body_len < 2 * 1024 * 1024) {
				$connection->send((string) $response . file_get_contents($file, false, null, $offset, $body_len), true);
				return '';
			}
			$handler = \fopen($file, 'r');
			if (false === $handler) {
				$connection->close(new Response(403, null, '403 Forbidden'));
				return '';
			}
			$connection->send((string) $response, true);
			static::sendStream($connection, $handler, $offset, $length);
			return '';
		}
		
		return (string) $response;
	}

	/**
	 * Send remainder of a stream to client.
	 *
	 * @param TcpConnection $connection        	
	 * @param
	 *        	$handler
	 * @param
	 *        	$offset
	 * @param
	 *        	$length
	 */
	protected static function sendStream(TcpConnection $connection, $handler, $offset = 0, $length = 0) {
		$connection->bufferFull = false;
		if ($offset !== 0) {
			\fseek($handler, $offset);
		}
		$offset_end = $offset + $length;
		// Read file content from disk piece by piece and send to client.
		$do_write = function () use ($connection, $handler, $length, $offset_end) {
			// Send buffer not full.
			while ($connection->bufferFull === false) {
				// Read from disk.
				$size = 1024 * 1024;
				if ($length !== 0) {
					$tell = \ftell($handler);
					$remain_size = $offset_end - $tell;
					if ($remain_size <= 0) {
						fclose($handler);
						$connection->onBufferDrain = null;
						return;
					}
					$size = $remain_size > $size ? $size : $remain_size;
				}
				
				$buffer = \fread($handler, $size);
				// Read eof.
				if ($buffer === '' || $buffer === false) {
					fclose($handler);
					$connection->onBufferDrain = null;
					return;
				}
				$connection->send($buffer, true);
			}
		};
		// Send buffer full.
		$connection->onBufferFull = function ($connection) {
			$connection->bufferFull = true;
		};
		// Send buffer drain.
		$connection->onBufferDrain = function ($connection) use ($do_write) {
			$connection->bufferFull = false;
			$do_write();
		};
		$do_write();
	}

	/**
	 * Set or get uploadTmpDir.
	 *
	 * @return bool|string
	 */
	public static function uploadTmpDir($dir = null) {
		if (null !== $dir) {
			static::$_uploadTmpDir = $dir;
		}
		if (static::$_uploadTmpDir === '') {
			if ($upload_tmp_dir = \ini_get('upload_tmp_dir')) {
				static::$_uploadTmpDir = $upload_tmp_dir;
			} else if ($upload_tmp_dir = \sys_get_temp_dir()) {
				static::$_uploadTmpDir = $upload_tmp_dir;
			}
		}
		return static::$_uploadTmpDir;
	}
}
